package com.sml.aop.sml;

import com.alibaba.fastjson.JSON;
import com.sml.requset.ApiRequest;
import com.sml.response.LogExceptionVO;
import com.sml.response.LogRedisVO;
import com.sml.service.alert.SMLAlertService;
import com.sml.util.DateUtil;
import com.dianping.cat.Cat;
import com.dianping.cat.message.Transaction;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * @Author linlx
 * @Date 2021/8/12 8:42
 * @Description
 **/
@Aspect
@Slf4j
public class SMLLogAspect {

  private ThreadLocal<ApiRequest> threadLocal = new ThreadLocal<>();

  private ExecutorService executorService;

  // 服务名
  private String serviceName;

  // 日志过期时间
  private Long expireMinutes;

  // 异常堆栈信息的层级
  private Integer exceptionLogDepth;

  private Task task = new Task();

  private ReentrantLock lock = new ReentrantLock();

  @Resource
  private StringRedisTemplate stringRedisTemplate;

  @Resource
  private ApplicationContext applicationContext;

  public SMLLogAspect(String serviceName, Long expireMinutes, Integer exceptionLogDepth) {
    this.serviceName = serviceName;
    this.expireMinutes = expireMinutes;
    this.exceptionLogDepth = exceptionLogDepth;
    this.executorService = Executors.newSingleThreadExecutor(
        new ThreadFactoryBuilder().setNameFormat("SMLLogAspect-" + this.serviceName + "-%d")
            .build());
    // 注册JVMHook，保证重启时会先发送完内存中尚未发送的异常日志信息
    Runtime.getRuntime().addShutdownHook(new Thread(() -> task.shutdown()));
  }

  @Pointcut("@annotation(SMLLogAnnotation)")
  private void pointcut() {

  }

  @Before(value = "pointcut() && args(apiRequest)")
  public void beforeAdvice(ApiRequest<?> apiRequest) {
    // 请求参数放入线程变量
    threadLocal.set(apiRequest);
  }

  @Around("pointcut()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    String methodName = joinPoint.getSignature().getName();
    Class clazz = joinPoint.getTarget().getClass();
    Object[] params = joinPoint.getArgs();
    ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder
        .getRequestAttributes();
    assert sra != null;
    HttpServletRequest request = sra.getRequest();
    String requestURI = request.getRequestURI();
    Transaction t = Cat.newTransaction(serviceName, requestURI);
    try {
      Object result = joinPoint.proceed();
      t.setStatus(Transaction.SUCCESS);
      return result;
    } catch (Exception e) {
      String feignId = threadLocal.get().getRequestId();
      // 对异常的日志添加链路唯一标识
      t.addData(feignId);
      t.setStatus(e);
      // 异常日志信息redis持久化
      this.sendRedisLog(methodName, params, e, feignId, clazz);
      throw e;
    } finally {
      t.complete();
    }
  }

  private void sendRedisLog(String methodName, Object[] params, Exception e, String feignId,
      Class clazz) {
    // 异步写日志
    executorService.execute(() -> {
      LogRedisVO logRedisVO = new LogRedisVO();
      logRedisVO.setParams(params);
      // 异常名称
      logRedisVO.setExceptionName(e.toString());
      // 发生异常的原始方法
      logRedisVO.setOriginMethodName(clazz.getName() + "." + methodName);
      logRedisVO.setCreateTime(DateUtil.getFormatDate(new Date()));
      StackTraceElement[] stackTrace = e.getStackTrace();
      Integer index = Math.min(exceptionLogDepth, stackTrace.length);
      List<LogExceptionVO> logExceptionVOList = new ArrayList<>(index);
      LogExceptionVO logExceptionVO;
      for (int i = 0; i < index; i++) {
        logExceptionVO = new LogExceptionVO();
        // 发生异常的类
        logExceptionVO.setClassName(stackTrace[i].getClassName());
        // 发生异常的行数
        logExceptionVO.setLineNumber(stackTrace[i].getLineNumber());
        // 发生异常的方法
        logExceptionVO.setExceptionMethodName(stackTrace[i].getMethodName());
        logExceptionVOList.add(logExceptionVO);
      }
      logRedisVO.setLogExceptionVOList(logExceptionVOList);
      // key为请求唯一标识 键值对为服务名-异常日志信息
      stringRedisTemplate.opsForHash().put(feignId, serviceName, JSON.toJSONString(logRedisVO));
      stringRedisTemplate.expire(feignId, expireMinutes, TimeUnit.MINUTES);
      // 只在异常的原始服务进行异常日志的发送，防止多次发送
      if (feignId.contains(serviceName)) {
        task.addTask(feignId);
      }
    });
  }

  @Data
  class Task {

    private List<String> feignIdList = new ArrayList<>();

    private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

    public Task() {
      executorService.scheduleAtFixedRate(() -> {
        try {
          lock.tryLock();
          // 异常链路日志通知
          this.alertLog();
        } finally {
          feignIdList.clear();
          lock.unlock();
        }
      }, 5, 30, TimeUnit.SECONDS);
    }


    public void addTask(String feignId) {
      lock.tryLock();
      feignIdList.add(feignId);
      lock.unlock();
    }

    public void shutdown() {
      this.alertLog();
    }

    private void alertLog() {
      if (feignIdList.size() == 0) {
        return;
      }
      // 异常消息发送
      List<SMLAlertService> alertServiceList = new ArrayList(
          applicationContext.getBeansOfType(SMLAlertService.class).values());
      alertServiceList.forEach(service -> service.alert(feignIdList));
    }
  }
}
